#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright (c) 2011 - 2013 Stefano Mazzucco <stefano -at- curso.re>
# All rights reserved.
#
# This file is part of Crystal Ball Plus.
#
# Crystal Ball Plus is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Crystal Ball Plus is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Crystal Ball Plus.  If not, see <http://www.gnu.org/licenses/>.

"""Functions used to match a unknown structure to one or more
reference structures

"""

import os
import copy
import numpy as np

from structures import *
from utilities import *

def _check_d_spacings(data):
    """Removes entries that don't have d_spacings from list 'data' in place.

    This function is used by function 'compare_d'

    *Parameters*

    data : list of 2-tuples
           the 1st element is a string representing the file name,
           the 2nd element is a Diffractogram instance

    """
    bad_idx = []
    for i in xrange(len(data)):
        if len(data[i][1].d_spacings) == 0:
            print('Error reading d-spacings from file %s'
                  % data[i][1].filename)
            bad_idx.append(i)
    for i in bad_idx:
        data.pop(i)

def _comp_d(inpobj, refobj, matchobj):
    """Return a structured array containing input and reference indices
    relative to the Miller planes of inpobj and refobj, and updates the
    attributes of inpobj in place.

    *Parameters*

    inpobj : Diffractogram

    refobj : Diffractogram

    matchobj : structured array
               names : 'inp_idx', 'ref_idx', 'err'
               formats : 'i4', 'i4', 'i4', 'i4'

    *Returns*

    result : structured array
             names : 'inp_idx', 'ref_idx', 'err'
             formats : 'i4', 'i4', 'i4', 'i4'

    """
    result = []
    for val in matchobj:
        ref_idx = val['ref_idx']
        inp_idx = val['inp_idx']
        err = val['err']
        result.append((inp_idx, ref_idx, err))
    result = np.array(result,
                      dtype={'names':['inp_idx',
                                      'ref_idx',
                                      'err'],
                             'formats':['i4',
                                        'i4',
                                        'f4']})
    if result['ref_idx'].size > 0:
        setattr(inpobj, 'unitcell', refobj.unitcell)
        setattr(inpobj, 'sg', refobj.sg)
        setattr(inpobj, 'sg_name', refobj.sg_name)
        setattr(inpobj, 'sg_number', refobj.sg_number)
        setattr(inpobj, 'uc_ang_units',
                refobj.uc_ang_units)
        setattr(inpobj, 'uc_len_units',
                refobj.uc_len_units)
        setattr(inpobj, 'structure', refobj.structure)
        setattr(inpobj, 'chemname', refobj.chemname)
        setattr(inpobj, 'spacegroup', refobj.sg_name)
        setattr(inpobj, 'crystsys', refobj.crystsys)
        inpobj.update_info()
        inpobj.update_rmt()
        inpobj.update_dmt()
        return result

def compare_d(inp_path, ref_path, minerr=0.01,
              inp_ext='.dfg', ref_ext='.dfg', gen=False):
    """Return a 'dictionary of dictionaries' containing all the valid
    matches between the input d-spacing data in inp_path and the
    reference d-spacing data in ref_path and the corresponding Miller indices
    (if present in the reference data). Also, return the input and
    reference data as instances of class Diffractogram.

    Input and reference files MUST be of 'diffractogram' type (see method
    read_file of class Diffractogram for more information)

    *Parameters*

    inp_path : string
               path to file or folder containing input files

    ref_path : string
               path to file or folder containing reference files

    minerr : float (optional)
             minimum error within which a d-spacing should be considered.
             Defaults to 0.01 (i.e. 1%)

    inp_ext : string (optional)
              extension of input files, defaults to '.dfg'

    ref_ext : string
              extension of reference files, defaults to '.dfg'

    gen : bool (optional)
          if False (default), it will read the reference Miller planes and
          d-spacings from file;
          if True, it will generate all the valid Miller planes from (0, 0, 0)
          to (5, 5, 5) and compute the d-spacings using the information
          contained in the unit cell present in the file

    *Returns*

     match_d : dictionary of dictionaries
               match_d[key1][key2]

               key1: input file
               key2: reference file
               value: 3-tuple, where

                          value[0] : structured array
                                     keys = 'inp_idx', 'ref_idx', 'err'
                                     input index : int
                                     reference index : int
                                     relative error : float (0.1 means 10%)

                          value[1] : instance of Diffractogram
                                     from the input data

                          value[2] : instance of Diffractogram
                                     from the reference data
    *See also*

    Diffractogram, match, compare_angles, compare_za, group_reflections

    """
    if not os.path.exists(inp_path):
        print('Input path "%s" is not valid. Exiting.' % inp_path)
        return
    if not os.path.exists(ref_path):
        print('Reference path "%s" is not valid. Exiting.' % ref_path)
        return
    inp = []
    if os.path.isdir(inp_path):
        for root, dirs, files in os.walk(inp_path):
            for f in files:
                if os.path.splitext(f)[1] == inp_ext:
                    inp.append((f, Diffractogram(os.path.join(root, f))))
    elif os.path.isfile(inp_path):
        if os.path.splitext(inp_path)[1] == inp_ext:
            inp.append((os.path.basename(inp_path), Diffractogram(inp_path)))
    ref = []
    if os.path.isdir(ref_path):
        for root, dirs, files in os.walk(ref_path):
            for f in files:
                if os.path.splitext(f)[1] == ref_ext:
                    refobj = Diffractogram(os.path.join(root, f))
                    if gen:
                        refobj.gen_d_spacings()
                    ref.append((f, refobj))
    elif os.path.isfile(ref_path):
        if os.path.splitext(ref_path)[1] == ref_ext:
            refobj = Diffractogram(ref_path)
            if gen:
                refobj.gen_d_spacings()
            ref.append((os.path.basename(ref_path), refobj))
    _check_d_spacings(inp)
    _check_d_spacings(ref)
    match_d = {}
    for i in inp:
        match_d[i[0]] = {}
        for r in ref:
            # the same input d-spacing may have multiple matches
            m = match(i[1].d_spacings, r[1].d_spacings, err=minerr)
            match_d[i[0]][r[0]] = (m, i[1], r[1])
    for inpfile in match_d:
        for reffile in match_d[inpfile]:
            # inp Diffractogram:
            inpobj = copy.deepcopy(match_d[inpfile][reffile][1])
            # ref Diffractogram:
            refobj = match_d[inpfile][reffile][2]
            # output of 'match':
            matchobj = match_d[inpfile][reffile][0]
            result = None
            if hasattr(refobj, 'hkl'):
                if np.any(refobj.hkl):
                    result = _comp_d(inpobj, refobj, matchobj)
            if result is not None:
                match_d[inpfile][reffile] = (result, inpobj, refobj)
            else:
                match_d[inpfile][reffile] = None
    return match_d

def _group_refl(mtc):
    """Return a dictionary containing groups of reflections.

    *Parameters*

    mtc : structured array
          must have at least the keys 'inp_idx' and 'ref_idx'

    *Returns*

    groups : dictionary
             {key : value} -> group_id : group

             group_id : int, e.g. 21
             group : structured array

             structured array names:
             'spot_0', 'spot_1',
             'ref_0', 'ref_1'
             1st reflection : int, 2nd reflection : int,
             1st reference index : int, 2nd reference index : int

    """
    m_spot_0 = []
    m_ref_0 = []
    m_spot_1 = []
    m_ref_1 = []

    for i in xrange(mtc.size):
        for j in xrange(mtc.size):
            if i < j and mtc['inp_idx'][i] != mtc['inp_idx'][j]:
                m_spot_0.append(mtc['inp_idx'][i])
                m_spot_1.append(mtc['inp_idx'][j])
                m_ref_0.append(mtc['ref_idx'][i])
                m_ref_1.append(mtc['ref_idx'][j])
    spots = set(m_spot_0 +  m_spot_1)
    out = np.array(zip(m_spot_0, m_spot_1, m_ref_0, m_ref_1),
                   dtype = {'names' : ['spot_0', 'spot_1', 'ref_0', 'ref_1'],
                            'formats' : ['i4', 'i4', 'i4', 'i4']})

    # generate grouped pairs
    pairs = {}
    for i in spots:
        for j in spots:
            if i < j:
                pairs[(i, j)] = []

    # populate pairs
    for i in xrange(out.size):
        pair = (out['spot_0'][i], out['spot_1'][i])
        if pair in pairs:
            if out[i] not in pairs[pair]:
                # convert to tuple to avoid ending up with a 0-rank array
                pairs[pair].append(tuple(out[i]))

    groups = {}
    group_id = 0
    n_pairs = len(pairs.keys())
    n_planes = np.max(mtc['inp_idx']) + 1

    # populate groups using dynamic code
    # A priori, we don't know how many pairs we are going to have,
    # so we need dynamic code here.
    # If e.g. we have 3 paris, the code is going to look like this:
    # '''
    # plist_0 = pairs[(0, 1)]
    # plist_1 = pairs[(1, 2)]
    # plist_2 = pairs[(0, 2)]
    # for mtc_0 in plist_0:
    #     for mtc_1 in plist_1:
    #         for mtc_2 in plist_2:
    #             planes = set([ mtc_0[-1], mtc_0[-2],
    #                            mtc_1[-1], mtc_1[-2],
    #                            mtc_2[-1], mtc_2[-2]])
    #             if len(planes) == n_planes:
    #                 groups[group_id] = np.array(
    #                     [mtc_0, mtc_1, mtc_2],
    #                     dtype={"names" : ["spot_0", "spot_1",
    #                                       "ref_0", "ref_1"],
    #                            "formats" : ["i4", "i4",
    #                                         "i4", "i4"]})
    #                 group_id += 1
    # '''
    code = ''
    for idx, key in enumerate(pairs.keys()):
        code += 'plist_' + repr(idx) + ' = pairs[' + repr(key) + ']\n'
    for i in xrange(n_pairs):
        code = code + '    ' * i + 'for mtc_' + repr(i)
        code = code + ' in plist_%i:\n' % i
    code += '    ' * n_pairs
    code += 'planes = set([ '
    for i in xrange(n_pairs):
        code += 'mtc_%i[-1], mtc_%i[-2], ' % (i, i)
    code = code.rstrip(', ') + '])\n'
    code += '    ' * n_pairs
    code += 'if len(planes) == n_planes:\n'
    code += '    ' * (n_pairs + 1)
    code += 'groups[group_id] = np.array(['
    for i in xrange(n_pairs):
        code += 'mtc_%i, ' %i
    code = code.rstrip(', ') + ']'
    code += ', dtype={"names" : ["spot_0", "spot_1", "ref_0", "ref_1"],'
    code +='"formats" : ["i4", "i4", "i4", "i4"]})\n'
    code += '    ' * (n_pairs + 1)
    code += 'group_id += 1\n'
    exec code
    return groups

def group_reflections(match_hkl, minerr=0.05):
    """Return a dictionary of dictionaries containing the indexes of the
    reflections grouped by pairs.

    Usually, match_hkl is the output of compare_d

    *Parameters*

    match_hkl : dictionary of dictionaries
                match_d[key1][key2]

                key1: input file
                key2: reference file
                value: 3-tuple, where

                          value[0] : structured array
                                         names : 'inp_idx', 'ref_idx'
                                         formats : int, int
                                         desc. : input index, reference index

                          value[1] : instance of Diffractogram
                                     from the input data

                          value[2] : instance of Diffractogram
                                     from the reference data

    *Returns*

    pairs : dictionary of dictionaries
            pairs[key1][key2]

            key1: input file
            key2: reference file
            value: 3-tuple, where

                  value[0] : dictionary
                            {key : value} -> group_id : group

                            group_id : int, e.g. 21
                            group : structured array
                            structured array
                            names: 'spot_0', 'spot_1', 'ref_0', 'ref_1'
                            1st reflection : int, 2nd reflection : int,
                            1st reference index : int, 2nd reference index : int

                 value[1] : instance of Diffractogram
                            from the input data

                 value[2] : instance of Diffractogram
                            from the reference data

    *See also*

    compare_d, match, Diffractogram, compare_za, compare_angles

    """
    reflections = {}
    for inpfile in match_hkl:
        reflections[inpfile] = {}
        for reffile in match_hkl[inpfile]:
            if match_hkl[inpfile][reffile] is not None:
                mtc =  match_hkl[inpfile][reffile][0]
                inpobj = match_hkl[inpfile][reffile][1] # Diffractogram
                refobj = match_hkl[inpfile][reffile][2] # Diffractogram
                grp = _group_refl(mtc)
                if grp:
                    reflections[inpfile][reffile] = (grp, inpobj, refobj)
                else:
                    reflections[inpfile][reffile] = None
            else:
                reflections[inpfile][reffile] = None
    return reflections

def _comp_a(group, inpobj, refobj, minerr, check):
    """Return the common zone axis - if any - to a group of reflections.

    *Parameters*

    group : structured array
            names: 'spot_0', 'spot_1', 'ref_0', 'ref_1'
            formats : 'i4', 'i4', 'i4', 'i4'

    inpobj : Diffractogram
             input object

    refobj : Diffractogram
             reference object

    minerr : float
             minimum error within which an angle should be considered.

    check : function
            E.g. Numpy's 'all' or 'any'

    *Returns*

    angles : dictionary of structured arrays
             {key : value} -> {group_id : group}
             group_id : int, e.g. 0
             group : structured array
                     names: 'spot_0', 'spot_1', 'hkl_0', 'hkl_1',
                            'inp_ang', 'ref_ang', 'err', 'zone_axis'
                     formats : 'i4', 'i4', 'MillerPlane', 'MillerPlane',
                               'f4', 'f4', 'f4', 'array'

    """
    za_list = []
    za_list2 = []
    idx_list = []
    fam_0_list = []
    fam_1_list = []
    spot_0_list = []
    spot_1_list = []
    # find all zone axes
    for pair in group:
        idx_za = 0
        za = []
        idx = []
        spot_0 = []
        spot_1 = []
        fam_hkl_0 = refobj.family(refobj.hkl[pair['ref_0']])
        fam_hkl_1 = refobj.family(refobj.hkl[pair['ref_1']])
        s0 = pair['spot_0']
        s1 = pair['spot_1']
        a0 = inpobj.angles_to_x[s0]
        a1 = inpobj.angles_to_x[s1]
        ia = float(abs(a0 - a1)) # input angle
        for idx_f0, hkl_0 in enumerate(fam_hkl_0):
            for idx_f1, hkl_1 in enumerate(fam_hkl_1):
                ra = refobj.angle(hkl_0, hkl_1) # reference angle
                dv = abs((ia - ra) / ra)        # deviation (percent error)
                if not np.all(hkl_0 == hkl_1) and dv < minerr:
                    za1 = hkl_0.zone_axis(hkl_1)
                    za2 = hkl_1.zone_axis(hkl_0)
                    assert np.all(za1 == za2), \
                        "Z.A. mismatch %s != %s" % (za1, za2)
                    za.append(za1)
                    idx.append((idx_f0, idx_f1, idx_za))
                    spot_0.append(s0)
                    spot_1.append(s1)
                    idx_za += 1
        za_list2.append(za[:])  # must make a copy of za!
        remove_duplicates(za)
        za_list.append(za)
        idx_list.append(idx)
        fam_0_list.append(fam_hkl_0)
        fam_1_list.append(fam_hkl_1)
        spot_0_list.append(spot_0)
        spot_1_list.append(spot_1)
    if len(za_list) > 1:
        za = intersect(*za_list)
    else:
        za = za_list
    if za:
        # group zone axes
        possible_za = [None for i in group]
        for j, za_pair in enumerate(za_list2):
            pair_match = []
            for k, z in enumerate(za_pair):
                for za_itrsct in za:
                    if np.all(za_itrsct == z):
                        spot_0 = spot_0_list[j][idx_list[j][k][2]]
                        spot_1 = spot_1_list[j][idx_list[j][k][2]]
                        hkl_0 = fam_0_list[j][idx_list[j][k][0]]
                        hkl_1 = fam_1_list[j][idx_list[j][k][1]]
                        za_m = za_list2[j][idx_list[j][k][2]]
                        z1 = hkl_0.zone_axis(hkl_1)
                        pair_match.append((spot_0, spot_1, hkl_0, hkl_1, z))
                possible_za[j] = pair_match
        lpz = len(possible_za)
        if None in possible_za:
            return
        za_match = []
        idx_grp = 0
        za_grp = {}
        if lpz == 1:
            for z0 in possible_za[0]:
                za_grp[idx_grp] = np.array([z0, ],
                                           dtype={
                        'names' : ['spot_0', 'spot_1',
                                   'hkl_0', 'hkl_1',
                                   'zone_axis'],
                        'formats' : ['i4', 'i4',
                                     'object', 'object',
                                     'object']})
                idx_grp += 1
        else:
            # dynamic code here to deal with unknown number of pairs
            # it will look like this for e.g. 3 pairs
            # for z0 in possible_za[0]:
            #     for z1 in possible_za[1]:
            #             for z2 in possible_za[2]:
            #                 if np.all(
            #                     np.asarray(z0[-1]) == np.asarray(z1[-1]))\
            #                         and np.all(
            #                     np.asarray(z1[-1]) == np.asarray(z2[-1])):
            #                         p0 = [z0[2].hkl, z0[3].hkl]
            #                         p1 = [z1[2].hkl, z1[3].hkl]
            #                         p2 = [z2[2].hkl, z2[3].hkl]
            #                         p_u = len(union(p0, p1, p2))
            #                         p_i = len(intersect(p0, p1, p2))
            #
            #                         # col_0  |   col_1  |  col_2  | col_3
            #                         # -------|----------|---------|-------
            #                         # spot_0 |  spot_1  |  hkl_0  | hkl_1
            #                         #    0   |     1    |     A   |    B
            #                         #    1   |     2    |     B   |    C
            #                         #    0   |     2    |     A   |    C
            #                         #         (this one is OK)
            #                         # col_0  |   col_1  |  col_2  | col_3
            #                         # -------|----------|---------|-------
            #                         #        |          |         |
            #                         # spot_0 |  spot_1  |  hkl_0  | hkl_1
            #                         #    0   |     1    |     A   |    B
            #                         #    1   |     2    |     A   |    C
            #                         #    0   |     2    |     B   |    C
            #                         #         (this one is NOT OK)
            #
            #                         col_0 = (z0[0], z1[0], z2[0])
            #                         col_1 = (z0[1], z1[1], z2[1])
            #                         col_2 = (z0[2], z1[2], z2[2])
            #                         col_3 = (z0[3], z1[3], z2[3])
            #                         if p_u == 3 and p_i == 0:
            #                             is_col_0_ok = False
            #                             is_col_1_ok = False
            #                             for i in xrange(3):
            #                                 for j in xrange(3):
            #                                     if i < j:
            #                                         if np.all(
            #                                             col_2[i] == col_2[j]):
            #                                             if col_0[i] == col_0[j]:
            #                                                 is_col_0_ok = True
            #                                         if np.all(
            #                                             col_3[i] == col_3[j]):
            #                                             if col_1[i] == col_1[j]:
            #                                                 is_col_1_ok = True
            #                             if is_col_0_ok and is_col_1_ok:
            #                                 za_grp[idx_grp] = np.array(
            #                                     [z0, z1, z2],
            #                                     dtype={
            #                                         'names' :
            #                                             ['spot_0', 'spot_1',
            #                                              'hkl_0', 'hkl_1',
            #                                              'zone_axis'],
            #                                         'formats' :
            #                                             ['i4', 'i4',
            #                                              'object', 'object',
            #                                              'object']})
            #                                 idx_grp += 1
            code = ''
            n_pairs = len(possible_za)
            for i in xrange(n_pairs):
                code += '    ' * i + 'for z' + repr(i)
                code += ' in possible_za[' + repr(i) + ']:\n'
            code += '    ' * n_pairs + 'if'
            for i in xrange(n_pairs - 1):
                code += ' np.all('
                code += 'np.asarray(z' + repr(i) + '[-1]) == np.asarray(z' + repr(i + 1)
                code += '[-1])) and'
            code = code.rstrip(' and') + ':\n'
            for i in xrange(n_pairs):
                code += '    ' * (n_pairs + 1)
                code += 'p' + repr(i) + ' = [z' + repr(i) + '[2].hkl, '
                code += 'z' + repr(i) + '[3].hkl]\n'
            code += '    ' * (n_pairs + 1) + 'p_u = len(union('
            for i in xrange(n_pairs):
                code += 'p' + repr(i) + ', '
            code = code.rstrip(', ') + '))\n'
            code += '    ' * (n_pairs + 1) + 'p_i = len(intersect('
            for i in xrange(n_pairs):
                code += 'p' + repr(i) + ', '
            code = code.rstrip(', ') + '))\n'
            for i in xrange(4):
                code += '    ' * (n_pairs + 1)
                code += 'col_' + repr(i) + ' = ('
                for j in xrange(n_pairs):
                    code += 'z' + repr(j) + '[' + repr(i) + '], '
                code = code.rstrip(', ') + ')\n'
            code += '    ' * (n_pairs + 1)
            code += 'if p_u == ' + repr(n_pairs) + ' and p_i == 0:\n'
            for i in xrange(2):
                code += '    ' * (n_pairs + 2)
                code += 'is_col_' + repr(i) + '_ok = False\n'
            code += '    ' * (n_pairs + 2) + 'for i in xrange(' + repr(n_pairs) + '):\n'
            code += '    ' * (n_pairs + 3) + 'for j in xrange(' + repr(n_pairs) + '):\n'
            code += '    ' * (n_pairs + 4) + 'if i < j:\n'
            code += '    ' * (n_pairs + 5) + 'if np.all(col_2[i] == col_2[j]):\n'
            code += '    ' * (n_pairs + 6) + 'if col_0[i] == col_0[j]:\n'
            code += '    ' * (n_pairs + 7) + 'is_col_0_ok = True\n'
            code += '    ' * (n_pairs + 5) + 'if np.all(col_3[i] == col_3[j]):\n'
            code += '    ' * (n_pairs + 6) + 'if col_1[i] == col_1[j]:\n'
            code += '    ' * (n_pairs + 7) + 'is_col_1_ok = True\n'
            code += '    ' * (n_pairs + 2) + 'if is_col_0_ok and is_col_1_ok:\n'
            code += '    ' * (n_pairs + 3) + 'za_grp[idx_grp] = np.array(['
            for i in xrange(n_pairs):
                code += 'z' + repr(i) + ', '
            code = code.rstrip(', ')
            code += "], dtype={ 'names':['spot_0', 'spot_1', 'hkl_0', 'hkl_1', "
            code += "'zone_axis'],"
            code += "'formats':['i4', 'i4','object', 'object', 'object']})\n"
            code +='    ' * (n_pairs + 3) + 'idx_grp += 1'
            exec code

        if za_grp:
            # group angles
            a_grp = {}
            idx_a = 0
            for group in za_grp.itervalues():
                angles = [None for i in group]
                for idx, pairs in enumerate(group):
                    a0 = inpobj.angles_to_x[pairs['spot_0']]
                    a1 = inpobj.angles_to_x[pairs['spot_1']]
                    ia = float(abs(a0 - a1))
                    ra = refobj.angle(pairs['hkl_0'], pairs['hkl_1'])
                    dv = abs((ia - ra) / ra)
                    s0 = pairs['spot_0']
                    s1 = pairs['spot_1']
                    p0 = pairs['hkl_0']
                    p1 = pairs['hkl_1']
                    za = pairs['zone_axis']
                    if check(np.asarray(dv) < minerr):
                        angles[idx] = (s0, s1, p0, p1, ia, ra, dv, za)
                if None not in angles:
                    angles =  np.array(angles,
                                       dtype={'names' : ['spot_0', 'spot_1',
                                                         'hkl_0', 'hkl_1',
                                                         'inp_ang', 'ref_ang',
                                                         'err', 'zone_axis'],
                                              'formats' : ['i4', 'i4',
                                                           'object', 'object',
                                                           'f4', 'f4',
                                                           'f4', 'object']})
                    a_grp[idx_a] = angles
                    idx_a += 1
            del za_grp
            if a_grp:
                return a_grp

def compare_angles(pairs, minerr=0.05, check='any'):
    """Return a dictionary of dictionaries containing the matching angles
    (within a given error) between measured angles and the Miller planes
    in dictionary match_za.

    *Parameters*

    pairs : dictionary of dictionaries
            pairs[key1][key2]

            key1: input file
            key2: reference file
            value: 3-tuple, where

                  value[0] : dictionary
                             group : structured array
                             names: 'spot_0', 'spot_1', 'ref_0', 'ref_1'
                             1st reflection : int, 2nd reflection : int,
                             1st reference index : int,2nd reference index : int

                 value[1] : instance of Diffractogram
                            from the input data

                 value[2] : instance of Diffractogram
                            from the reference data

    minerr : float (optional)
             minimum error within which an angle should be considered.
             Defaults to 0.05 (i.e. 5%)

    check : string (optional)
            can be either 'any' (default) or 'all'. Specifies whether any or
            all the angles must satisfy the condition 'minerr'.

    *Returns*

    match_a : dictionary of dictionaries
               match_a[key1][key2]

               key1: input file
               key2: reference file
               value: 3-tuple, where

                  value[0] : dictionary of structured arrays
                             {key : value} -> group_id  : group

                             group_id : in, e.g. 21
                             group : structured array

                                names:'spot_0', 'spot_1',
                                      'hkl_0', 'hkl_1',
                                      'inp_ang', 'ref_ang',
                                      'err', 'zone_axis'
                                1st reflection : int, 2nd reflection : int,
                                1st Miller plane : MillerPlane,
                                2nd Miller plane : MillerPlane,
                                input angle : float,
                                reference angle : float,
                                error : float (0.1 means 10%),
                                zone axis : array

                  value[1] : instance of Diffractogram
                             from the input data

                  value[2] : instance of Diffractogram
                             from the reference data

    *See also*

    Diffractogram, compare_d, group_reflections

    """
    chk = {'any' : np.any, 'all' : np.all}
    check = chk[check] # raises KeyError if check != 'any' | 'all'
    match_a = {}
    for inpfile in pairs:
        match_a[inpfile] = {}
        for reffile in pairs[inpfile]:
            if pairs[inpfile][reffile] is not None:
                groups = pairs[inpfile][reffile][0] # dictionary
                inpobj = pairs[inpfile][reffile][1] # Diffractogram
                refobj = pairs[inpfile][reffile][2] # Diffractogram
                za_groups = {}
                za_gid = 0
                for gid, grp in groups.iteritems():
                    mtc = _comp_a(grp, inpobj, refobj, minerr, check) # dict
                    if mtc is not None:
                        for angles in mtc.itervalues():
                            za_groups[za_gid] = angles
                            za_gid += 1
                if za_groups:
                        match_a[inpfile][reffile] = (za_groups, inpobj, refobj)
                else:
                    match_a[inpfile][reffile] = None
            else:
                match_a[inpfile][reffile] = None
    return match_a
